package com.openess.bigsearch.engine.index;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Map.Entry;

import com.openess.bigsearch.engine.BTree.util.ByteArrays;
import com.openess.bigsearch.engine.utils.HashMap;
import com.openess.bigsearch.engine.utils.Map;

/**
 * Nrm类，用于读写nrm文件，文件内写有：1.docNum。2.avLength。3.(id, norm,
 * docLength)对，即每个文档及其对应的向量模长以及每篇文档的字符长度，并提供按id进行搜索文件的功能
 * 。目前是全部装载进内存，因为即使是1000万篇文档，其占用的内存也只有100M，但如果只在内存出保存(id,
 * offset),虽然节省内存，但是假设查询3个分词
 * ，总共返回了1000篇文档，那么就需要1000次磁盘访问，将耗费大量的时间。因此全部装载，为了节省内存，以后可以考虑进行压缩优化。
 * 
 * @author TuTu
 * 
 */
public class Nrm {
	/**
	 * 数据包装类，主要包装有三类数据：(id, norm, docLength)
	 * 
	 * @author TuTu
	 * 
	 */
	public class NrmData {
		/**
		 * 一个Data所占的字节数
		 */
		private static final int size = 12;
		/**
		 * 文档特征项向量模长
		 */
		private double norm;
		/**
		 * 文档字符长度
		 */
		private int docLength;

		/**
		 * 构造方法
		 * 
		 * @param norm
		 * @param docLength
		 */
		public NrmData(double norm, int docLength) {
			this.norm = norm;
			this.docLength = docLength;
		}

		@Override
		public String toString() {
			String str = "norm = " + norm + ", length = " + docLength;
			return str;
		}
	}

	/**
	 * 头信息，目前头信息数据包括：1.int存储的size。2.docNum。3.avLength
	 */
	public static final int HEAD = 12;
	/**
	 * 一个(docID, norm, docLength)所占的大小
	 */
	public static final int DATASIZE = 8 + NrmData.size;
	/**
	 * 索引中包含的文档个数
	 */
	private int docNum;
	/**
	 * 索引文档的总字符长度，方便合并，avLength = totalLength/docNum
	 */
	private int totalLength;
	/**
	 * 将数据从磁盘中读取出来，放入散列表中，提供搜索服务。现在也负责写入磁盘。
	 */
	private HashMap<Long, NrmData> map;
	private String mode;
	private RandomAccessFile rf;
	private FileChannel cf;
	/**
	 * <Long, Data>的总个数
	 */
	private int size;

	/**
	 * 装有头信息的字节数组，头数据包括：1.<Long, Data>的总个数。2.docNum。3.avLength
	 */
	private byte[] head;

	/**
	 * 构造方法，如果模式为读，那么预先将nrm文件中的数据全部加载到内存中，等待搜索
	 * 
	 * @param filePath
	 * @param mode
	 *            判断Nrm对象是用于读还是用于写，写为"w"，读为"r"，读写一起操作，则为"rw"
	 * @param version
	 * @throws IOException
	 */
	public Nrm(String filePath, String mode, int version) throws IOException {
		this.mode = mode;
		rf = new RandomAccessFile(filePath + "nrm_" + version, "rw");
		cf = rf.getChannel();
		head = new byte[HEAD];
		// 预先读取头信息是为了文件的续写
		if (cf.size() != 0) {
			readHead();
		}
		map = new HashMap<Long, NrmData>(500);
		if (mode.contains("r")) {
			read();
		}
	}

	/**
	 * 添加数据，准备写入磁盘
	 * 
	 * @param t
	 * @return
	 */
	public void add(long docID, double norm, int docLength) {
		map.put(docID, new NrmData(norm, docLength));
	}

	public void close() throws IOException {
		if (cf != null) {
			cf.close();
		}
		if (rf != null) {
			rf.close();
		}
	}

	public final int getDocNum() {
		return docNum;
	}

	public final HashMap<Long, NrmData> getMap() {
		return map;
	}

	public final int getTotalLength() {
		return totalLength;
	}

	/**
	 * 从磁盘中将数据读取出来，并放入散列表，待用于搜索
	 * 
	 * @throws IOException
	 */
	public void read() throws IOException {
		if (cf.size() == 0) {
			return;
		}
		for (int i = 0; i < size; i++) {
			byte[] data = new byte[DATASIZE];
			ByteBuffer buf = ByteBuffer.wrap(data);
			cf.read(buf, HEAD + i * DATASIZE);
			long docID = ByteArrays.getLong(data, 0);
			Double norm = ByteArrays.getDouble(data, 8);
			Integer length = ByteArrays.getInt(data, 16);
			map.put(docID, new NrmData(norm, length));
		}
		// 只读模式才可以在读取后，关闭流
		if (!mode.contains("w")) {
			close();
		}
	}

	/**
	 * 读取磁盘中数据的头信息，并将相关变量进行赋值
	 * 
	 * @throws IOException
	 */
	private void readHead() throws IOException {
		ByteBuffer buf = ByteBuffer.wrap(head);
		cf.read(buf, 0);
		size = ByteArrays.getInt(head, 0);
		docNum = ByteArrays.getInt(head, 4);
		totalLength = ByteArrays.getInt(head, 8);
	}

	/**
	 * 从散列表中，依据id搜索出其对应的文档长度
	 * 
	 * @param id
	 * @return 如果没有对应文档的模长，则返回空
	 */
	public Integer searchLength(long id) {
		NrmData d = map.get(id);
		if (d == null) {
			return null;
		}
		return d.docLength;
	}

	/**
	 * 从散列表中，依据id搜索出其对应的模长
	 * 
	 * @param id
	 * @return 如果没有对应文档的模长，则返回空
	 */
	public Double searchNorm(long id) {
		NrmData d = map.get(id);
		if (d == null) {
			return null;
		}
		return d.norm;
	}

	public final void setDocNum(int docNum) {
		this.docNum = docNum;
	}

	public final void setTotalLength(int totalLength) {
		this.totalLength = totalLength;
	}

	/**
	 * 将链表中的数据写入磁盘，之后再将变化的头信息也写入磁盘
	 * 
	 * @throws IOException
	 */
	public void write() throws IOException {
		if (map.isEmpty()) {
			return;
		}
		// 从上次的结束为止开始写
		long offset;
		if (size == 0) {
			offset = HEAD;
		} else {
			offset = size * DATASIZE;
		}
		size += map.size();
		// 将集合中的数据取出，并写入磁盘
		for (Map.Entry<Long, NrmData> e : map.entrySet()) {
			long id = e.getKey();
			NrmData d = e.getValue();
			double norm = d.norm;
			int length = d.docLength;
			byte[] data = new byte[DATASIZE];
			ByteArrays.putLong(id, data, 0);
			ByteArrays.putDouble(norm, data, 8);
			ByteArrays.putInt(length, data, 16);
			ByteBuffer buf = ByteBuffer.wrap(data);
			cf.write(buf, offset);
			offset += DATASIZE;
		}
		map = null;
		// 写入变化后的头信息
		writeHead();
		close();
	}

	/**
	 * 往磁盘中写入头数据
	 * 
	 * @throws IOException
	 */
	private void writeHead() throws IOException {
		ByteArrays.putInt(size, head, 0);
		ByteArrays.putInt(docNum, head, 4);
		ByteArrays.putInt(totalLength, head, 8);
		ByteBuffer buf = ByteBuffer.wrap(head);
		cf.write(buf, 0);
	}
}
